#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/seq_file.h>
#include <linux/proc_fs.h>
#include <asm/uaccess.h>

#include "scull.h"
#include "scull_common.h"

struct scull_dev *scull_devices;

int scull_major     = SCULL_MAJOR;
int scull_minor     = SCULL_MINOR;
int scull_nr_devs   = SCULL_NR_DEVS;
int scull_quantum   = SCULL_QUANTUM;
int scull_qset      = SCULL_QSET;

module_param(scull_major, int, S_IRUGO);
module_param(scull_minor, int, S_IRUGO);
module_param(scull_nr_devs, int, S_IRUGO);
module_param(scull_quantum, int, S_IRUGO);
module_param(scull_qset, int, S_IRUGO);

static struct file_operations scull_fops = {
    .owner  = THIS_MODULE,
    .open   = scull_open,
    .read   = scull_read,
    .write  = scull_write,
    .release= scull_release,
    .ioctl  = scull_ioctl,
};

#ifdef LDD_DEBUG /* use proc only if debugging */
/*
 * The proc filesystem: function to read and entry
 */

int scull_read_procmem(char *buf, char **start, off_t offset,
        int count, int *eof, void *data)
{
    int i, j, len = 0;
    int limit = count - 80; /* Don't print more than this */

    for (i = 0; i < scull_nr_devs && len <= limit; i++) {
        struct scull_dev *d = &scull_devices[i];
        struct scull_qset *qs = d->data;
        if (down_interruptible(&d->sem))
            return -ERESTARTSYS;
        len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n",
                i, d->qset, d->quantum, d->size);
        for (; qs && len <= limit; qs = qs->next) { /* scan the list */
            len += sprintf(buf + len, "  item at %p, qset at %p\n",
                    qs, qs->data);
            if (qs->data && !qs->next) /* dump only the last item */
                for (j = 0; j < d->qset; j++) {
                    if (qs->data[j])
                        len += sprintf(buf + len,
                                "    % 4i: %8p\n",
                                j, qs->data[j]);
                }
        }
        up(&scull_devices[i].sem);
    }
    *eof = 1;
    return len;
}

static void scull_create_proc(void)
{
	create_proc_read_entry("scullmem", 0 /* default mode */,
			NULL /* parent dir */, scull_read_procmem,
			NULL /* client data */);
}

static void scull_remove_proc(void)
{
	/* no problem if it was not registered */
	remove_proc_entry("scullmem", NULL /* parent dir */);
}
#endif

static int scull_trim(struct scull_dev *dev)
{
    struct scull_qset *dptr, *next;
    int i;

    PDEBUG("dev=0x%p", dev);
    for (dptr=dev->data; dptr; dptr=dptr->next) {
        if (dptr->data) {
            for (i=0; i<dev->qset; i++) {
                kfree(dptr->data[i]);
            }
            kfree(dptr->data);
            dptr->data = NULL;
        }
        next = dptr->next;
        kfree(dptr);
    }
    dev->size = 0;
    dev->quantum = scull_quantum;
    dev->qset = scull_qset;
    dev->data = NULL;

    return 0;
}

int scull_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev;

    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = dev;

    PDEBUG("open scuccess");

    if ((filp->f_flags & O_ACCMODE) == O_WRONLY) {
        if ((down_interruptible(&dev->sem))) {
            return -ERESTARTSYS;
        }
        scull_trim(dev);
        up(&dev->sem);
    }
    return 0;
}

int scull_release(struct inode *node, struct file *filp)
{
    PDEBUG("release success");
    return 0;
}

static struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
    struct scull_qset *qs = dev->data;

    PDEBUG("dev=%p, dev->data=%p, n=%d", dev, dev->data, n);

    if (qs == NULL) {
        qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
        if (qs == NULL) {
            return NULL;
        }
        PDEBUG("qs=0x%p", qs);
        memset(qs, 0, sizeof(struct scull_qset));
    }

    while (n--) {
        if (qs->next == NULL) {
            qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
            if (qs->next == NULL) {
                return NULL;
            }
            memset(qs->next, 0, sizeof(struct scull_qset));
        }
        qs = qs->next;
    }

    return qs;
}

ssize_t scull_read(struct file *filp, char * __user buf, size_t count,
        loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr = NULL;
    int quantum = dev->quantum;
    int qset = dev->qset;
    int itemsize = quantum * qset;
    int item, rest, s_pos, q_pos;
    ssize_t retval = 0;

    if (down_interruptible(&dev->sem)) {
        return -ERESTARTSYS;
    }

    PDEBUG("*f_pos=%d, count=%d, dev->size=%d", (int)(*f_pos), (int)count, 
            (int)(dev->size));

    if (*f_pos >= dev->size) {
        goto out;
    }

    if (*f_pos + count > dev->size) {
        count = dev->size - *f_pos;
    }

    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;

    s_pos = (long)*f_pos / quantum;
    q_pos = (long)*f_pos % quantum;

    PDEBUG("item=%d, rest=%d, s_pos=%d, q_pos=%d", item, rest, s_pos, q_pos);

    dptr = scull_follow(dev, item);
    if (dptr==NULL || !dptr->data || !dptr->data[s_pos]) {
        PDEBUG("dptr=0x%p, null pointer error", dptr);
        goto out;
    }

    if (count + q_pos > quantum) {
        count = quantum - q_pos;
    }

    if (copy_to_user(buf, dptr->data[s_pos]+q_pos, count)) {
        retval = -EFAULT;
        goto out;
    }

    *f_pos += count;
    retval = count;

    PDEBUG("*f_pos=%d, count=%d, dev->size=%d", (int)(*f_pos), (int)count, 
            (int)(dev->size));
    PDEBUG("read success");
out:
    up(&dev->sem);
    return retval;
}

ssize_t scull_write(struct file *filp, const char * __user buf, size_t count,
        loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr = NULL;
    int quantum = dev->quantum;
    int qset = dev->qset;
    int itemsize = quantum * qset;
    int item, rest, s_pos, q_pos;
    ssize_t retval = -ENOMEM;

    if (down_interruptible(&dev->sem)) {
        return -ERESTARTSYS;
    }

    PDEBUG("*f_pos=%d, count=%d, dev->size=%d", (int)(*f_pos), (int)count, 
            (int)(dev->size));

    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;

    s_pos = (long)*f_pos / quantum;
    q_pos = (long)*f_pos % quantum;

    PDEBUG("item=%d, rest=%d, s_pos=%d, q_pos=%d", item, rest, s_pos, q_pos);

    dptr = scull_follow(dev, item);
    if (dptr == NULL) {
        PDEBUG("dptr==NULL");
        goto out;
    }
    if (dptr->data == NULL) {
        dptr->data = kmalloc(sizeof(char *) * qset, GFP_KERNEL);
        PDEBUG("dptr=0x%p, dptr->data=0x%p", dptr, dptr->data);
        if (!dptr->data) {
            PDEBUG("dptr->data==NULL");
            goto out;
        }
        memset(dptr->data, 0, sizeof(char *) * qset);
    }
    
    if (dptr->data[s_pos] == NULL) {
        dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
        if (!dptr->data[s_pos]) {
            PDEBUG("dptr->data[%d]==NULL", s_pos);
            goto out;
        }
        PDEBUG("dptr=0x%p, dptr->data=0x%p, dptr->data[%d]=0x%p", dptr, 
                dptr->data, s_pos, dptr->data[s_pos]);
        memset(dptr->data[s_pos], 0, quantum);
    }

    if (count + q_pos > quantum) {
        count = quantum - q_pos;
    }

    if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
        PDEBUG("copy_from_user error");
        retval = -EFAULT;
        goto out;
    }

    PDEBUG("*f_pos=%d, count=%d, dev->size=%d", (int)(*f_pos), (int)count, 
            (int)(dev->size));

    *f_pos += count;
    retval = count;

    if (dev->size < *f_pos) {
        dev->size = *f_pos;
    }

    PDEBUG("*f_pos=%d, count=%d, dev->size=%d", (int)(*f_pos), (int)count, 
            (int)(dev->size));
out:
    PDEBUG("dptr=0x%p, dptr->data=0x%p, dptr->data[q_pos]=0x%p", dptr, 
            dptr->data, dptr->data[q_pos]);
    up(&dev->sem);
    return retval;
}

int scull_ioctl(struct inode *inode, struct file *filp,
                 unsigned int cmd, unsigned long arg)
{

	int err = 0, tmp;
	int retval = 0;
    
	/*
	 * extract the type and number bitfields, and don't decode
	 * wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok()
	 */
	if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;
	if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;

	/*
	 * the direction is a bitmask, and VERIFY_WRITE catches R/W
	 * transfers. `Type' is user-oriented, while
	 * access_ok is kernel-oriented, so the concept of "read" and
	 * "write" is reversed
	 */
	if (_IOC_DIR(cmd) & _IOC_READ)
		err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
	else if (_IOC_DIR(cmd) & _IOC_WRITE)
		err =  !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
	if (err) return -EFAULT;

	switch(cmd) {

	  case SCULL_IOCRESET:
		scull_quantum = SCULL_QUANTUM;
		scull_qset = SCULL_QSET;
		break;
        
	  case SCULL_IOCSQUANTUM: /* Set: arg points to the value */
		if (! capable (CAP_SYS_ADMIN))
			return -EPERM;
		retval = __get_user(scull_quantum, (int __user *)arg);
		break;

	  case SCULL_IOCTQUANTUM: /* Tell: arg is the value */
		if (! capable (CAP_SYS_ADMIN))
			return -EPERM;
		scull_quantum = arg;
        PDEBUG("scull_quantum=%d", scull_quantum);
		break;

	  case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result */
		retval = __put_user(scull_quantum, (int __user *)arg);
		break;

	  case SCULL_IOCQQUANTUM: /* Query: return it (it's positive) */
		return scull_quantum;

	  case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer */
		if (! capable (CAP_SYS_ADMIN))
			return -EPERM;
		tmp = scull_quantum;
		retval = __get_user(scull_quantum, (int __user *)arg);
		if (retval == 0)
			retval = __put_user(tmp, (int __user *)arg);
		break;

	  case SCULL_IOCHQUANTUM: /* sHift: like Tell + Query */
		if (! capable (CAP_SYS_ADMIN))
			return -EPERM;
		tmp = scull_quantum;
		scull_quantum = arg;
		return tmp;
        
	  case SCULL_IOCSQSET:
		if (! capable (CAP_SYS_ADMIN))
			return -EPERM;
		retval = __get_user(scull_qset, (int __user *)arg);
		break;

	  case SCULL_IOCTQSET:
		if (! capable (CAP_SYS_ADMIN))
			return -EPERM;
		scull_qset = arg;
		break;

	  case SCULL_IOCGQSET:
		retval = __put_user(scull_qset, (int __user *)arg);
		break;

	  case SCULL_IOCQQSET:
		return scull_qset;

	  case SCULL_IOCXQSET:
		if (! capable (CAP_SYS_ADMIN))
			return -EPERM;
		tmp = scull_qset;
		retval = __get_user(scull_qset, (int __user *)arg);
		if (retval == 0)
			retval = put_user(tmp, (int __user *)arg);
		break;

	  case SCULL_IOCHQSET:
		if (! capable (CAP_SYS_ADMIN))
			return -EPERM;
		tmp = scull_qset;
		scull_qset = arg;
		return tmp;

	  default:  /* redundant, as cmd was checked against MAXNR */
		return -ENOTTY;
	}
	return retval;

}


static void scull_setup_cdev(struct scull_dev *devices, int index)
{
    int err;
    dev_t dev = MKDEV(scull_major, scull_minor+index);

    cdev_init(&devices->cdev, &scull_fops);
    devices->cdev.ops = &scull_fops;
    devices->cdev.owner = THIS_MODULE;
    err=cdev_add(&devices->cdev, dev, 1);
    if (err) {
        printk(KERN_ERR "Error %d adding scull%d\n", err, index);
    }
}

void scull_cleanup_module(void)
{
    int i;
    dev_t dev = MKDEV(scull_major, scull_minor);

    if (scull_devices) {
        for (i=0; i<scull_nr_devs; i++) {
            scull_trim(&scull_devices[i]);
            cdev_del(&scull_devices[i].cdev);
        }
        kfree(scull_devices);
    }
#ifdef LDD_DEBUG /* use proc only if debugging */
	scull_remove_proc();
#endif
    unregister_chrdev_region(dev, scull_nr_devs);
    printk(KERN_INFO "scull_cleanup_module success\n");
}

int scull_init_module(void)
{
    int i, result;
    dev_t dev = 0;

    if (scull_major) {
        dev = MKDEV(scull_major, scull_minor);
        result = register_chrdev_region(dev, scull_nr_devs, "scull");
    } else {
        result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
        scull_major = MAJOR(dev);
        PDEBUG("dev=%d, scull_major=%d ", dev, scull_major);
    }

    if (result < 0) {
        printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
        return result;
    }

    scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
    if (scull_devices == NULL) {
        goto fail;
    }
    memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

    for (i=0; i<scull_nr_devs; i++) {
        scull_devices[i].quantum = scull_quantum;
        scull_devices[i].qset    = scull_qset;
        init_MUTEX(&scull_devices[i].sem);
        scull_setup_cdev(scull_devices, i);
        PDEBUG("i=%d, scull_quantum=%d, scull_qset=%d ", i, scull_quantum, scull_qset);
    }
    printk(KERN_INFO "scull_init_module success\n");
#ifdef LDD_DEBUG /* use proc only if debugging */
    scull_create_proc();
#endif
    return 0;
fail:
    scull_cleanup_module();
    return result;
}

module_init(scull_init_module);
module_exit(scull_cleanup_module);

MODULE_AUTHOR("Cheng Zhou");
MODULE_LICENSE("GPL");

